Modelo de Markowitz vs Macacos lançando dardos¶
O objetivo dessa análise é comparar (de forma simples) o retorno de uma carteira de ações baseando-se na fronteira eficiente de Markowitz com uma carteira selecionada aleatoriamente.
O modelo de Markowitz se baseia na de maximizar o retorno com base em determinado nível de risco assumido, selecionando a distribuição "ótima" de pesos entre os ativos da carteira. Em suma, essa estratégia se dá conforme o gráfico abaixo:
Para realizar o experimento, outra ideia também será usada: a de que a seleção aleatória de ações, com pesos iguais, bate o mercado a longo prazo. A estratégia já foi debatida e avaliada em diversos estudos e artigos (1, 2 e 3, por exemplo) e é bastante divulgada como "macacos jogando dardos" por conta de uma frase do professor Burton Malkiel em seu livro best-seller "A Random Walk Down Wall Street": “Um macaco vendado jogando dardos nas páginas financeiras de um jornal poderia selecionar um portfólio que funcionaria tão bem quanto aquele cuidadosamente selecionado por especialistas”.
Dois motivos geralmente atribuídos para esses resultados são:
Os retornos de ações serem altamente assimétricos, ou seja, eles não seguem uma distribuição simétrica, como uma curva normal (onde os retornos positivos e negativos estariam distribuídos de forma equilibrada em torno de uma média). Em vez disso, há uma inclinação em direção a um dos lados – ou seja, um lado da distribuição de retornos é mais frequente ou mais extremo do que o outro.
A alocação uniforme (mesmo peso para todos ativos) tende a gerar mais retornos, fenômeno geralmente atriuído à diversificação "obrigatória" que esse modelo propões, à diluição dos ricos e exposição mais alta a ativos com menor valor e maior potencial de crescimento.
Dados¶
Para realizar o experimento, os seguintes dados foram usados
- Ações disponíveis em 2014
- Composição do IBovespa (maio de 2014)
Metodologia¶
A ideia é utilizar dados entre 2014 e 2019 para selecionar uma carteira a ser mantida até 2024. Para isso, será feita uma comparação sobre o retorno final, em 2024, de uma carteira montada com o modelo de Markowitz e a média de retorno e risco das carteiras escolhidas aleatoriamente, conforme o seguinte:
- Markowitz
- Escolha de 10 ativos com participação no Ibovespa em 2014 com maior crescimento entre janeiro e fevereiro de 2014;
- Geração de 50 mil carteiras para o período de 2014 a 2019, com pesos gerados aleatoriamente;
- Escolha da melhor carteira baseada no Sharpe Ratio.
- Macacos
- Geração de 50 mil carteiras, com ativos escolhidos aleatoriamente e pesos iguais, para o período de 2014 a 2024;
Ao fim, será possível checar quantas carteiras aleatórias bateram a carteira com o modelo de Markowitz, seguindo um critério de escolha mais simples.
Além disso, para simplicar ainda mais a análise, não serão consideradas falências ou fusões e aquisições, ou seja, só ações ON existentes entre 2014 e 2024 que tenham mantido o nome original.
Bibliotecas¶
import random
import pandas as pd
import plotly.graph_objects as go
from tqdm import tqdm
from IPython import display
import pickle
import numpy as np
import plotly.express as px
import yfinance as yf
from scipy import optimize
import matplotlib.pyplot as plt
from scipy.stats import norm
def read_txt_file(filename):
f = open(filename, 'r')
f_content = f.read().strip()
f.close()
return f_content
Lista de ações disponíveis em 2021¶
filename = 'cotacoes_2014_b3.txt'
content = read_txt_file(filename)
rows = content.split('\n')
rows[:3]
['012014010202AAPL34 010APPLE DRN R$ 000000001321200000000132120000000013212000000001321200000000132120000000013190000000001334000001000000000000000900000000000011890800000000000000009999123100000010000000000000BRAAPLBDR004106', '012014010202ABCB4 010ABC BRASIL PN EJS N2 R$ 000000000123100000000012360000000001170000000000119200000000011900000000001175000000000119001544000000000000361200000000000430789400000000000000009999123100000010000000000000BRABCBACNPR4121', '012014010296ABCB4F 020ABC BRASIL PN EJS N2 R$ 000000000121000000000012100000000001176000000000117900000000011760000000001152000000000124900003000000000000000143000000000000168614000000000000009999123100000010000000000000BRABCBACNPR4121']
rows[0][12:17]
'AAPL3'
# Coletando ações ON
tickers = list(set([t[12:17] for t in rows if t[16] == '3' and t[12:16].isalpha() and t[17] == " "]))
tickers[:10]
['HYPE3', 'SSBR3', 'CTAX3', 'CIEL3', 'TGMA3', 'ECOR3', 'ENBR3', 'QUAL3', 'BRPR3', 'ENEV3']
# Coletando dados das ações disponíveis na API do Yahoo que contenham dados de 2014 a 2024
# available_tickers = {}
# for ticker in tqdm(tickers):
# data = yf.Ticker(f"{ticker}.SA").history(period="max").reset_index()
# if len(data) and data['Date'].dt.year.eq(2014).any() and data['Date'].dt.year.eq(2024).any():
# available_tickers[ticker] = data
# display.clear_output(wait=True)
# f = open('available_tickers.pkl', 'wb')
# pickle.dump(available_tickers, f)
# f.close()
available_tickers = pickle.load(open('available_tickers.pkl', 'rb'))
print("Número de ações disponíveis:", len(available_tickers))
Número de ações disponíveis: 127
# Coletando dados do IBovespa
ibovespa = yf.Ticker("^BVSP").history(period="max")
ibovespa = ibovespa.reset_index()
ibovespa = ibovespa[ibovespa['Date'] >= '2014-01-01'].reset_index(drop=True)
ibovespa_until_2019 = ibovespa[ibovespa['Date'] <= '2019-12-31'].reset_index(drop=True)
Simulando carteira - Markowitz¶
filename = 'ibovespa_2014.txt'
content = read_txt_file(filename)
ibovespa_2014 = content.split('\n')
ibovespa_2014[:10]
['ALLL3', 'ABEV3', 'AEDU3', 'BVMF3', 'BBAS3', 'BBSE3', 'BRML3', 'BRPR3', 'BBDC4', 'BBDC3']
# Coletando ações ON
ibovespa_2014 = [t for t in ibovespa_2014 if t[-1] == '3']
ibovespa_2014[:10]
['ALLL3', 'ABEV3', 'AEDU3', 'BVMF3', 'BBAS3', 'BBSE3', 'BRML3', 'BRPR3', 'BBDC3', 'BRFS3']
jan_2014_growths = {}
for ticker in ibovespa_2014:
if ticker in available_tickers:
jan_start = available_tickers[ticker][available_tickers[ticker]['Date'] == '2014-01-02']['Close'].values[0]
jan_end = available_tickers[ticker][available_tickers[ticker]['Date'] == '2014-02-28']['Close'].values[0]
jan_2014_growths[ticker] = float(jan_end/jan_start)
jan_2014_growths = dict(sorted(jan_2014_growths.items(), key=lambda x:x[1], reverse=True))
print('Lista de ações com maior valorização entre jan/2014 e fev/2014:')
for ticker, growth in list(jan_2014_growths.items())[:10]:
print(f'{ticker}: {growth:.2f}')
Lista de ações com maior valorização entre jan/2014 e fev/2014: EMBR3: 1.09 MRFG3: 1.06 DASA3: 1.05 MRVE3: 1.03 GFSA3: 1.01 BBSE3: 1.00 LREN3: 0.98 ABEV3: 0.98 UGPA3: 0.97 RENT3: 0.95
markowitz_portifolio_tickers = [ticker for ticker, _ in list(jan_2014_growths.items())[:10]]
markowitz_portifolio_tickers
['EMBR3', 'MRFG3', 'DASA3', 'MRVE3', 'GFSA3', 'BBSE3', 'LREN3', 'ABEV3', 'UGPA3', 'RENT3']
markowitz_portifolio = {}
for ticker in markowitz_portifolio_tickers:
markowitz_portifolio[ticker] = available_tickers[ticker][
(available_tickers[ticker]['Date'] >= '2014-01-01') &
(available_tickers[ticker]['Date'] <= '2019-12-31')
].reset_index(drop=True)
fig = go.Figure([
go.Scatter(x=markowitz_portifolio[ticker]['Date'],
y=markowitz_portifolio[ticker]['Open'],
name=ticker)
for ticker in markowitz_portifolio_tickers] +
[go.Scatter(x=ibovespa_until_2019['Date'],
y=ibovespa_until_2019['Open'],
name="Ibovespa")
])
fig.update_layout(
title="Variação do preço das ações no portifólio",
xaxis_title="Anos",
yaxis_title="Preço",
template="plotly_white"
)
fig.show()
Retorno diário¶
Para calcular o retorno diários das ações, será usado o retorno logarítmico, dado por:
$ \Large{r} = \Large\log(\Large\frac{p_n}{p_{n-1}})$
onde $p_n$ é o preço do dia $n$ e $p_{n-1}$ o preço no dia $n-1$.
O uso dessa estratégia em relação ao retorno tradicional ($\Large{r} = \Large\frac{p_n}{p_{n-1}}$) garante algumas propriedades interessantes para análises, tais como:
Aditividade, o que significa que é possível somar os retornos logarítmicos diários para obter o retorno logarítmico total de um período mais longo. Para o retorno tradicional, seria necessário multiplicar fatores de retorno, o que é menos conveniente. Por exemplo, se $r_1$ e $r_2$ são retornos logarítmicos de dois dias consecutivos, o retorno total para os dois dias é simplesmente $𝑟_1 + r_2$, enquanto para retornos simples é $(1 + 𝑟_1) * (1 + r_2) - 1$;
Simetria. No caso de retornos logarítmicos, uma queda e um aumento de mesmo percentual têm impactos iguais em valor absoluto, ou seja, efeitos de aumento e queda percentuais são consistentes em ambos os sentidos. Com retornos simples, uma queda de 50% seguida de uma alta de 50% não leva ao ponto de partida original.
for ticker in markowitz_portifolio:
markowitz_portifolio[ticker]['Return'] = np.log(markowitz_portifolio[ticker]['Open'] / markowitz_portifolio[ticker]['Open'].shift(1))
ibovespa['Return'] = np.log(ibovespa['Open'] / ibovespa['Open'].shift(1))
ibovespa_until_2019 = ibovespa[ibovespa['Date'] <= '2019-12-31'].reset_index(drop=True)
fig = px.line(title = 'Histórico de retorno diário das ações')
for ticker in markowitz_portifolio:
fig.add_scatter(x = markowitz_portifolio[ticker]["Date"] ,y = markowitz_portifolio[ticker]['Return'], name = ticker)
fig.add_scatter(x = ibovespa_until_2019["Date"] ,y = ibovespa_until_2019['Return'], name = 'Ibovespa')
fig.show()
returns = pd.DataFrame()
for ticker in markowitz_portifolio_tickers:
returns[ticker] = markowitz_portifolio[ticker]['Return']
returns['IBOVESPA'] = ibovespa['Return']
returns
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | IBOVESPA | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
| 1 | 0.005746 | -5.064382e-02 | -0.013831 | -0.069668 | -0.088163 | -0.018167 | -0.036930 | 0.006338 | -0.026649 | -0.021168 | -0.023050 |
| 2 | 0.001561 | 5.802389e-02 | 0.027474 | 0.066119 | 0.071141 | -0.018928 | 0.006924 | -0.026777 | -0.017859 | 0.018772 | 0.012475 |
| 3 | 0.003115 | -2.453874e-03 | 0.002031 | -0.013119 | 0.058350 | -0.019726 | 0.001346 | 0.005296 | -0.003350 | -0.011160 | 0.000039 |
| 4 | 0.002589 | 3.144129e-02 | -0.015673 | -0.024303 | -0.041328 | -0.003906 | 0.005197 | 0.018029 | 0.002421 | -0.016823 | -0.010867 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1486 | -0.004652 | -4.000538e-02 | -0.034068 | 0.013831 | -0.035694 | 0.021109 | 0.010670 | 0.000997 | 0.027079 | 0.018546 | -0.007119 |
| 1487 | 0.017972 | -2.166150e-02 | 0.000000 | -0.037318 | 0.009377 | -0.014200 | 0.001079 | 0.020729 | 0.011443 | -0.015286 | -0.001756 |
| 1488 | 0.004063 | -4.179645e-03 | 0.000000 | -0.000951 | 0.079427 | 0.004756 | 0.005861 | -0.002634 | 0.028836 | 0.019188 | -0.003598 |
| 1489 | -0.000507 | 2.787990e-02 | 0.000000 | 0.047838 | 0.080422 | 0.001317 | 0.019487 | 0.015180 | 0.017222 | 0.029872 | -0.002584 |
| 1490 | 0.000000 | 3.856441e-08 | 0.017094 | -0.004545 | 0.000000 | -0.002108 | -0.015915 | -0.002601 | -0.002720 | -0.006028 | -0.003845 |
1491 rows × 11 columns
returns.describe()
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | IBOVESPA | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 1.490000e+03 | 1490.000000 | 1490.000000 | 1490.000000 | 1490.000000 | 1490.000000 | 1490.000000 | 1.490000e+03 | 1490.000000 | 1490.000000 | 1490.000000 |
| mean | 5.732175e-05 | 0.000594 | 0.000971 | 0.000957 | -0.000678 | 0.000551 | 0.001223 | 2.372574e-04 | 0.000039 | 0.001110 | 0.000542 |
| std | 2.152341e-02 | 0.029011 | 0.024720 | 0.023145 | 0.033118 | 0.019954 | 0.021430 | 1.467985e-02 | 0.018761 | 0.023033 | 0.014190 |
| min | -1.472207e-01 | -0.175353 | -0.136280 | -0.165815 | -0.301648 | -0.168740 | -0.187517 | -1.207816e-01 | -0.079867 | -0.175622 | -0.092048 |
| 25% | -1.211826e-02 | -0.015699 | -0.004246 | -0.012399 | -0.019657 | -0.010690 | -0.011700 | -8.126255e-03 | -0.010611 | -0.012673 | -0.007730 |
| 50% | -4.643918e-08 | -0.000898 | 0.000000 | 0.000633 | -0.000280 | 0.000598 | 0.000629 | 1.507075e-07 | 0.000147 | 0.000233 | 0.000476 |
| 75% | 1.258519e-02 | 0.016025 | 0.003720 | 0.015051 | 0.016184 | 0.011781 | 0.013795 | 8.980729e-03 | 0.010485 | 0.014520 | 0.008880 |
| max | 1.929781e-01 | 0.187397 | 0.248697 | 0.101664 | 0.221320 | 0.133485 | 0.108540 | 6.521183e-02 | 0.082861 | 0.098488 | 0.063760 |
returns.mean()*100
EMBR3 0.005732 MRFG3 0.059443 DASA3 0.097052 MRVE3 0.095707 GFSA3 -0.067810 BBSE3 0.055140 LREN3 0.122264 ABEV3 0.023726 UGPA3 0.003941 RENT3 0.110991 IBOVESPA 0.054180 dtype: float64
returns.std()*100
EMBR3 2.152341 MRFG3 2.901102 DASA3 2.471960 MRVE3 2.314490 GFSA3 3.311838 BBSE3 1.995441 LREN3 2.143000 ABEV3 1.467985 UGPA3 1.876140 RENT3 2.303267 IBOVESPA 1.418988 dtype: float64
returns.corr()
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | IBOVESPA | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| EMBR3 | 1.000000 | 0.127047 | -0.015081 | 0.167553 | 0.120547 | 0.160833 | 0.175058 | 0.235213 | 0.189568 | 0.171322 | 0.048359 |
| MRFG3 | 0.127047 | 1.000000 | 0.062380 | 0.323132 | 0.253249 | 0.346403 | 0.290278 | 0.280275 | 0.296639 | 0.296129 | 0.186902 |
| DASA3 | -0.015081 | 0.062380 | 1.000000 | 0.045565 | 0.060986 | 0.019819 | 0.003855 | 0.014246 | 0.037737 | 0.013923 | 0.033265 |
| MRVE3 | 0.167553 | 0.323132 | 0.045565 | 1.000000 | 0.340384 | 0.410745 | 0.418373 | 0.346911 | 0.385609 | 0.409621 | 0.240933 |
| GFSA3 | 0.120547 | 0.253249 | 0.060986 | 0.340384 | 1.000000 | 0.276662 | 0.277910 | 0.240620 | 0.223463 | 0.284021 | 0.218930 |
| BBSE3 | 0.160833 | 0.346403 | 0.019819 | 0.410745 | 0.276662 | 1.000000 | 0.485021 | 0.415721 | 0.423465 | 0.465517 | 0.280813 |
| LREN3 | 0.175058 | 0.290278 | 0.003855 | 0.418373 | 0.277910 | 0.485021 | 1.000000 | 0.413041 | 0.430498 | 0.499490 | 0.236527 |
| ABEV3 | 0.235213 | 0.280275 | 0.014246 | 0.346911 | 0.240620 | 0.415721 | 0.413041 | 1.000000 | 0.425773 | 0.379449 | 0.200587 |
| UGPA3 | 0.189568 | 0.296639 | 0.037737 | 0.385609 | 0.223463 | 0.423465 | 0.430498 | 0.425773 | 1.000000 | 0.398379 | 0.206542 |
| RENT3 | 0.171322 | 0.296129 | 0.013923 | 0.409621 | 0.284021 | 0.465517 | 0.499490 | 0.379449 | 0.398379 | 1.000000 | 0.242245 |
| IBOVESPA | 0.048359 | 0.186902 | 0.033265 | 0.240933 | 0.218930 | 0.280813 | 0.236527 | 0.200587 | 0.206542 | 0.242245 | 1.000000 |
fig = go.Figure(
data=go.Heatmap(
z=returns.corr().values,
x=returns.corr().columns,
y=returns.corr().columns,
colorscale='Viridis',
colorbar=dict(title="Correlação"),
text=returns.corr().round(2).values,
texttemplate="%{text}",
textfont=dict(color="white")
)
)
fig.update_layout(
title="Matriz de correlação das ações",
xaxis=dict(title="Ações"),
yaxis=dict(title="Ações"),
autosize=False,
width=600,
height=600,
)
fig.show()
corr_list = returns.corr().unstack().reset_index()
corr_list.columns = ['Ação1','Ação2','Corr']
# Removendo "auto-correlações"
corr_list = corr_list[corr_list['Ação1'] != corr_list['Ação2']]
corr_list.sort_values('Corr', ascending=False).head(20).iloc[1::2]
| Ação1 | Ação2 | Corr | |
|---|---|---|---|
| 75 | LREN3 | RENT3 | 0.499490 |
| 71 | LREN3 | BBSE3 | 0.485021 |
| 104 | RENT3 | BBSE3 | 0.465517 |
| 74 | LREN3 | UGPA3 | 0.430498 |
| 95 | UGPA3 | ABEV3 | 0.425773 |
| 63 | BBSE3 | UGPA3 | 0.423465 |
| 69 | LREN3 | MRVE3 | 0.418373 |
| 62 | BBSE3 | ABEV3 | 0.415721 |
| 73 | LREN3 | ABEV3 | 0.413041 |
| 38 | MRVE3 | BBSE3 | 0.410745 |
Carteira simulada - Monte Carlo + Markowitz¶
Modelo de Markowitz¶
O modelo de Markowitz é composto montando um portfólio de investimentos composto por múltiplos ativos, onde o retorno e o risco (volatilidade) são calculados de acordo com as proporções e correlações dos ativos.
Dessa forma, o retorno esperado é dado por:
$\Large{{E} (R_{p})=\sum _{i}w_{i}\operatorname {E} (R_{i})\quad}$
e a variância:
$ \Large{\sigma _{p}^{2}=\sum _{i}w_{i}^{2}\sigma _{i}^{2}+\sum _{i}\sum _{j\neq i}w_{i}w_{j}\sigma _{i}\sigma _{j}\rho _{ij}}$
- $\sigma_{i} $ é o desvio padrão do ativo $i$ (uma medida de sua volatilidade),
- $\rho_{ij}$ é a correlação entre os retornos dos ativos $𝑖$ e $𝑗$.
Essa formulação pode ser reescrita como:
$\Large{\sigma _{p}^{2}=\sum _{i}\sum _{j}w_{i}w_{j}\sigma _{ij}}$
onde:
- $\sigma_{ij} = \sigma_{i}\sigma_{j}\rho_{ij} $ é a covariância dos retornos dos ativo $i$ e $j$.
Além disso, é possível simplificar a notação organizando os retornos de N ativos em um vetor de dimensão $𝑁 × 1$ chamado $𝑅$, onde o primeiro elemento é o retorno do primeiro ativo, o segundo elemento é o retorno do segundo ativo, e assim por diante.
Organizando os retornos esperados desses ativos em um vetor coluna $\mu$ e suas variâncias e covariâncias em uma matriz de covariância $\Sigma$ e considerando um portfólio de ativos de risco cujos pesos para cada um dos $N$ ativos de risco são dados pelos elementos correspondentes no vetor de pesos $w$, tem-se:
- Retorno esperado do portfólio: $\Large{ w'\mu }$
- Variância do portfólio: $\Large{ w'\Sigma w}$
Simulação de Monte Carlo para uma carteira com modelo de Markowitz¶
Nesse contexto, é possível utilizar uma simulação Monte Carlo para encontrar valores aleatórios dos pesos. Com essa formulação, os melhores pesos serão selecionados a partir do Sharpe ratio, que permite avaliar a relação entre o retorno e o risco de um portfólio de investimentos.
Ele indica quanto retorno adicional (acima da taxa livre de risco) um investidor recebe por unidade de risco assumido. Em resumo, quanto maior o Índice de Sharpe, melhor o retorno ajustado pelo risco do portfólio. Sua fórmula é:
$\Large{S = \frac{E(R_p) - R_f}{\sigma_p}}$
onde:
- $E(R_p)$ é o retorno esperado do portfólio,
- $R_f$é a taxa de retorno livre de risco (como a taxa de um título do governo),
- $\sigma_p$ é a volatilidade (ou desvio padrão) dos retornos do portfólio, que representa o risco do portfólio.
portfolio = pd.DataFrame()
for ticker in markowitz_portifolio_tickers:
portfolio[ticker] = markowitz_portifolio[ticker]['Close']
portfolio
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 18.209976 | 2.531138 | 13.404299 | 4.433001 | 174.832977 | 10.809416 | 6.960318 | 11.417546 | 9.425428 | 8.177188 |
| 1 | 18.134100 | 2.603456 | 13.861689 | 4.793960 | 190.946594 | 10.574721 | 7.058234 | 11.174619 | 9.370058 | 8.339483 |
| 2 | 18.115133 | 2.675775 | 13.796346 | 4.703718 | 197.392044 | 10.425780 | 6.959138 | 11.030176 | 9.224706 | 8.167205 |
| 3 | 18.304813 | 2.728369 | 13.581655 | 4.568361 | 190.140915 | 10.358084 | 7.097166 | 11.378152 | 9.262771 | 8.112269 |
| 4 | 18.399660 | 2.715220 | 13.441636 | 4.557080 | 189.335236 | 10.488967 | 7.084190 | 11.489769 | 9.404667 | 8.139734 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1486 | 19.650000 | 6.239090 | 56.734615 | 18.977110 | 59.329472 | 24.972164 | 44.287331 | 15.930836 | 9.889529 | 41.415386 |
| 1487 | 19.730000 | 6.179921 | 56.734615 | 19.004288 | 63.595509 | 25.111115 | 44.597363 | 15.972958 | 10.178862 | 42.558605 |
| 1488 | 19.719999 | 6.370579 | 56.734615 | 19.502491 | 66.597534 | 24.985390 | 45.271854 | 16.191994 | 10.287361 | 43.886284 |
| 1489 | 19.719999 | 6.416600 | 56.734615 | 19.873882 | 67.782539 | 25.038332 | 44.658882 | 16.141449 | 10.335582 | 43.613468 |
| 1490 | 19.730000 | 6.548088 | 56.450939 | 19.520609 | 68.493546 | 24.945690 | 44.730534 | 15.728646 | 10.239138 | 43.113319 |
1491 rows × 10 columns
len(portfolio)
1491
num_simulations = 50000
portfolio_returns = np.log(portfolio/portfolio.shift(1))
simulated_weights = np.zeros((num_simulations, 10))
simulated_returns = np.zeros(num_simulations)
simulated_vol = np.zeros(num_simulations)
simulated_sharpe_ratio = np.zeros(num_simulations)
np.random.seed(42)
for i in tqdm(range(num_simulations)):
# Weights
weights = np.array(np.random.random(10))
weights = weights/np.sum(weights)
# Save weights
simulated_weights[i,:] = weights
# Expected return
simulated_returns[i] = np.sum((portfolio_returns.mean() * weights))
# Expected volatility
simulated_vol[i] = np.sqrt(np.dot(weights.T, np.dot(portfolio_returns.cov(), weights)))
# Sharpe Ratio
simulated_sharpe_ratio[i] = simulated_returns[i]/simulated_vol[i]
100%|██████████████████████████| 50000/50000 [08:16<00:00, 100.68it/s]
simulation = {'seed': 42, 'num_rounds': 50000,
'returns': simulated_returns, 'vol': simulated_vol, 'sharpe_ratio': simulated_sharpe_ratio,
'weights': simulated_weights}
f = open('portfolio_simulation.pkl', 'wb')
pickle.dump(simulation, f)
f.close()
simulation = pickle.load(open('portfolio_simulation.pkl', 'rb'))
simulated_returns = simulation['returns']
simulated_vol = simulation['vol']
simulated_sharpe_ratio = simulation['sharpe_ratio']
simulated_weights = simulation['weights']
print("Maior Sharpe Ratio: {}". format(simulated_sharpe_ratio.max()))
print("Posição do maior Sharpe Ratio: {}". format(simulated_sharpe_ratio.argmax()))
Maior Sharpe Ratio: 0.07218687093394922 Posição do maior Sharpe Ratio: 37823
best_weights = simulated_weights[simulated_sharpe_ratio.argmax(),:]
print("Melhores pesos para carteira:", best_weights)
Melhores pesos para carteira: [0.07411131 0.00671519 0.23927202 0.02543305 0.00759211 0.01852235 0.30892523 0.09297833 0.0119731 0.21447732]
best_return = simulated_returns[simulated_sharpe_ratio.argmax()]
best_vol = simulated_vol[simulated_sharpe_ratio.argmax()]
print("Melhor retorno (baseado no Sharpe Ratio):", best_return*100)
print("Melhor vol:", best_vol*100)
Melhor retorno (baseado no Sharpe Ratio): 0.09157722430092746 Melhor vol: 1.2686132965192571
plt.figure(figsize=(10, 7))
scatter = plt.scatter(
simulated_vol,
simulated_returns,
c=simulated_sharpe_ratio,
cmap='viridis',
s=100,
edgecolor='k'
)
plt.colorbar(scatter, label='Sharpe Ratio')
plt.scatter(
best_vol,
best_return,
color='red',
s=200,
edgecolor='k',
label="Sharpe Ratio máx."
)
# Add labels and title
plt.title('Retorno vs Vol com Sharpe Ratio')
plt.xlabel('Volatilidade')
plt.ylabel('Retorno')
plt.legend(loc="upper right")
# Show plot
plt.show()
Otimizando fronteira eficiente¶
Para determinar a fronteira eficiente, ou seja, definir quais portifólios entregam o melhor retorno para cada nível de risco (i.e., volatidade ou variância) é possível utilizar um método de otimização para funções não lineares.
Para isso, será utilizado um método de mínimos quadrados sequenciais (Sequential Least Squares Quadratic Programming, em inglês) que irá otimizar a função que representa a fonteira, tomando com base duas restrições:
- A soma dos pesos dos ativos deve ser igual a 1;
- Cada peso deve estar entre 0 e 1.
# Baseado no código de exemplo do professor João Gabriel de Moraes Souza
def get_portfolio_data(weights):
portfolio_data = {}
weights = np.array(weights)
portfolio_data['return'] = np.sum(portfolio_returns.mean() * weights)
portfolio_data['vol'] = np.sqrt(np.dot(weights.T, np.dot(portfolio_returns.cov(), weights)))
portfolio_data['sharpe_ratio'] = portfolio_data['return']/portfolio_data['vol']
return portfolio_data
# Função para negar Sharpe Ratio
# Como scipy.optimize minimiza funções e no Sharpe Ratio quanto maior o valor, melhor,
# é necessário inverter seu sinal para prepará-lo para minização
def get_negative_sharpe_ratio(weights):
return get_portfolio_data(weights)['sharpe_ratio'] * -1
def check_if_sum_is_1(weights):
return np.sum(weights)-1
constraints = ({'type': 'eq', 'fun': check_if_sum_is_1}) # Definição das restrições
bounds = ((0,1),)*10
initial_guess = ((0.1),)*10
opt_results = optimize.minimize(get_negative_sharpe_ratio, initial_guess, method="SLSQP", bounds= bounds, constraints=constraints)
print(opt_results)
message: Optimization terminated successfully
success: True
status: 0
fun: -0.07814897346555665
x: [ 1.145e-17 1.041e-03 2.552e-01 9.784e-02 2.064e-17
0.000e+00 4.592e-01 0.000e+00 1.865e-17 1.867e-01]
nit: 13
jac: [ 1.132e-02 2.974e-05 4.944e-04 -3.538e-04 9.854e-02
1.159e-02 -1.942e-04 1.368e-02 4.239e-02 -1.964e-05]
nfev: 144
njev: 13
frontier_y = np.linspace(0.000, 0.001, 100) # Retorna 100 pontos espaçados igualmente entre 0 e 0.0004
def minimize_volatility(weights):
return get_portfolio_data(weights)['vol']
frontier_x = []
for possible_return in tqdm(frontier_y):
constraints = ({'type':'eq', 'fun': check_if_sum_is_1},
{'type':'eq', 'fun': lambda w: get_portfolio_data(w)['return'] - possible_return})
result = optimize.minimize(minimize_volatility,initial_guess,method='SLSQP', bounds=bounds, constraints=constraints)
frontier_x.append(result['fun'])
100%|███████████████████████████████| 100/100 [03:52<00:00, 2.32s/it]
plt.figure(figsize=(10, 7))
scatter = plt.scatter(
simulated_vol,
simulated_returns,
c=simulated_sharpe_ratio,
cmap='viridis',
s=100,
edgecolor='k'
)
colorbar = plt.colorbar(scatter, label='Sharpe Ratio')
colorbar.ax.yaxis.set_label_coords(-0.4, 0.5)
plt.scatter(
best_vol,
best_return,
color='red',
s=200,
edgecolor='k',
label="Sharpe Ratio máx."
)
plt.plot(frontier_x, frontier_y, 'b--', linewidth=3, label="Fronteira")
plt.title('Fronteira eficiente')
plt.xlabel('Volatilidade')
plt.ylabel('Retorno')
plt.legend(loc="upper left")
plt.show()
def asset_allocation(stocks_df, total_money, optimal_weights):
stocks_df = stocks_df.copy()
stock_columns = stocks_df.columns
stocks_df[stock_columns] = stocks_df[stock_columns].div(stocks_df[stock_columns].iloc[0])
for i, stock in enumerate(stock_columns):
stocks_df[stock] = stocks_df[stock] * weights[i] * total_money
stocks_df['total_value'] = stocks_df[stock_columns].sum(axis=1)
stocks_df['daily_return'] = np.log(stocks_df['total_value'] / stocks_df['total_value'].shift(1)) * 100
asset_weights = pd.DataFrame({'stocks': stock_columns, 'weights': weights})
final_value = stocks_df['total_value'].iloc[-1]
return stocks_df, asset_weights, final_value
portfolio_until_2024 = pd.DataFrame()
for ticker in markowitz_portifolio_tickers:
portfolio_until_2024[ticker] = available_tickers[ticker][(available_tickers[ticker]['Date'] >= '2014-01-01') &
(available_tickers[ticker]['Date'] <= '2024-12-31')].reset_index(drop=True)['Close']
portfolio_until_2024_dates = available_tickers[ticker][(available_tickers[ticker]['Date'] >= '2014-01-01') &
(available_tickers[ticker]['Date'] <= '2024-12-31')].reset_index(drop=True)['Date']
portfolio_until_2024
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 18.209976 | 2.531138 | 13.404299 | 4.433001 | 174.832977 | 10.809416 | 6.960318 | 11.417546 | 9.425428 | 8.177188 |
| 1 | 18.134100 | 2.603456 | 13.861689 | 4.793960 | 190.946594 | 10.574721 | 7.058234 | 11.174619 | 9.370058 | 8.339483 |
| 2 | 18.115133 | 2.675775 | 13.796346 | 4.703718 | 197.392044 | 10.425780 | 6.959138 | 11.030176 | 9.224706 | 8.167205 |
| 3 | 18.304813 | 2.728369 | 13.581655 | 4.568361 | 190.140915 | 10.358084 | 7.097166 | 11.378152 | 9.262771 | 8.112269 |
| 4 | 18.399660 | 2.715220 | 13.441636 | 4.557080 | 189.335236 | 10.488967 | 7.084190 | 11.489769 | 9.404667 | 8.139734 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2702 | 53.790001 | 15.100000 | 2.480000 | 6.930000 | 1.490000 | 34.290001 | 16.969999 | 12.330000 | 20.530001 | 40.900002 |
| 2703 | 55.650002 | 15.400000 | 2.420000 | 7.100000 | 1.580000 | 34.200001 | 16.740000 | 12.510000 | 20.500000 | 42.570000 |
| 2704 | 54.130001 | 15.710000 | 2.340000 | 6.970000 | 1.480000 | 33.740002 | 16.719999 | 12.580000 | 20.430000 | 45.459999 |
| 2705 | 56.689999 | 15.610000 | 2.270000 | 6.930000 | 1.470000 | 33.540001 | 16.580000 | 12.730000 | 20.570000 | 45.180000 |
| 2706 | 55.419998 | 16.900000 | 2.150000 | 6.440000 | 1.400000 | 33.410000 | 16.490000 | 12.640000 | 19.290001 | 44.730000 |
2707 rows × 10 columns
stocks_values, stocks_weights, total_sum = asset_allocation(portfolio_until_2024, 35000, best_weights)
stocks_values
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | total_value | daily_return | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1084.106581 | 4166.976673 | 3576.127209 | 3877.782227 | 4886.783276 | 3373.418305 | 5015.326711 | 2472.283297 | 3370.715236 | 3176.480487 | 35000.000000 | NaN |
| 1 | 1079.589391 | 4286.032510 | 3698.154048 | 4193.531927 | 5337.177446 | 3300.174508 | 5085.881154 | 2419.681300 | 3350.913736 | 3239.525141 | 35990.661162 | 2.791143 |
| 2 | 1078.460235 | 4405.089917 | 3680.721243 | 4114.592938 | 5517.335200 | 3253.692770 | 5014.476326 | 2388.404622 | 3298.932923 | 3172.602503 | 35924.308678 | -0.184530 |
| 3 | 1089.752586 | 4491.676159 | 3623.443878 | 3996.188625 | 5314.657780 | 3232.565925 | 5113.933541 | 2463.753087 | 3312.545687 | 3151.262477 | 35789.779746 | -0.375182 |
| 4 | 1095.399159 | 4470.029108 | 3586.088412 | 3986.320939 | 5292.138115 | 3273.412151 | 5104.584119 | 2487.921946 | 3363.290526 | 3161.931379 | 35821.115853 | 0.087518 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2702 | 3202.315773 | 24858.917515 | 661.638157 | 6062.040120 | 41.647218 | 10701.273659 | 12227.902484 | 2669.860243 | 7341.924763 | 15887.864881 | 83655.384811 | -2.758655 |
| 2703 | 3313.048422 | 25352.802025 | 645.630798 | 6210.748245 | 44.162823 | 10673.186279 | 12062.174016 | 2708.836371 | 7331.195938 | 16536.586256 | 84878.371173 | 1.451351 |
| 2704 | 3222.557227 | 25863.151340 | 624.287588 | 6097.030218 | 41.367707 | 10529.629092 | 12047.762487 | 2723.993662 | 7306.162695 | 17659.224841 | 86115.166858 | 1.446625 |
| 2705 | 3374.963258 | 25698.522124 | 605.612336 | 6062.040120 | 41.088196 | 10467.212561 | 11946.884528 | 2756.473631 | 7356.229181 | 17550.457540 | 85859.483475 | -0.297350 |
| 2706 | 3299.355479 | 27822.230855 | 573.597617 | 5633.411210 | 39.131613 | 10426.641638 | 11882.034020 | 2736.985774 | 6898.476895 | 17375.651891 | 86687.516992 | 0.959785 |
2707 rows × 12 columns
fig = px.line(title = 'Evolução do patrimônio')
for i in stocks_values.drop(columns = ['total_value', 'daily_return']).columns:
fig.add_scatter(x = portfolio_until_2024_dates, y = stocks_values[i], name = i)
fig.show()
print(f"Valor final da carteira de Markowitz em 2024: R$ {total_sum:.2f}")
Valor final da carteira de Markowitz em 2024: R$ 86687.52
Value at Risk¶
O Value at Risk (VaR), ou Valor em Risco, é uma métrica estatística utilizada para medir o nível de risco de perdas potenciais em uma carteira de ativos financeiros ao longo de um horizonte de tempo específico, com um certo nível de confiança. O VaR fornece uma estimativa da perda máxima que uma carteira pode ter, com uma probabilidade específica, em condições normais de mercado.
Então, se um gestor de uma carteira, por exemplo, calcula que o VaR diário de uma carteira é de -0.1% ou R\$ 1 milhão com um nível de confiança de 99%, isso significa que, em condições normais de mercado, há 99% de chance de que a carteira não perca mais de 0.1% ou R\$ 1 milhão em um único dia.
stocks_values
| EMBR3 | MRFG3 | DASA3 | MRVE3 | GFSA3 | BBSE3 | LREN3 | ABEV3 | UGPA3 | RENT3 | total_value | daily_return | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1084.106581 | 4166.976673 | 3576.127209 | 3877.782227 | 4886.783276 | 3373.418305 | 5015.326711 | 2472.283297 | 3370.715236 | 3176.480487 | 35000.000000 | NaN |
| 1 | 1079.589391 | 4286.032510 | 3698.154048 | 4193.531927 | 5337.177446 | 3300.174508 | 5085.881154 | 2419.681300 | 3350.913736 | 3239.525141 | 35990.661162 | 2.791143 |
| 2 | 1078.460235 | 4405.089917 | 3680.721243 | 4114.592938 | 5517.335200 | 3253.692770 | 5014.476326 | 2388.404622 | 3298.932923 | 3172.602503 | 35924.308678 | -0.184530 |
| 3 | 1089.752586 | 4491.676159 | 3623.443878 | 3996.188625 | 5314.657780 | 3232.565925 | 5113.933541 | 2463.753087 | 3312.545687 | 3151.262477 | 35789.779746 | -0.375182 |
| 4 | 1095.399159 | 4470.029108 | 3586.088412 | 3986.320939 | 5292.138115 | 3273.412151 | 5104.584119 | 2487.921946 | 3363.290526 | 3161.931379 | 35821.115853 | 0.087518 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2702 | 3202.315773 | 24858.917515 | 661.638157 | 6062.040120 | 41.647218 | 10701.273659 | 12227.902484 | 2669.860243 | 7341.924763 | 15887.864881 | 83655.384811 | -2.758655 |
| 2703 | 3313.048422 | 25352.802025 | 645.630798 | 6210.748245 | 44.162823 | 10673.186279 | 12062.174016 | 2708.836371 | 7331.195938 | 16536.586256 | 84878.371173 | 1.451351 |
| 2704 | 3222.557227 | 25863.151340 | 624.287588 | 6097.030218 | 41.367707 | 10529.629092 | 12047.762487 | 2723.993662 | 7306.162695 | 17659.224841 | 86115.166858 | 1.446625 |
| 2705 | 3374.963258 | 25698.522124 | 605.612336 | 6062.040120 | 41.088196 | 10467.212561 | 11946.884528 | 2756.473631 | 7356.229181 | 17550.457540 | 85859.483475 | -0.297350 |
| 2706 | 3299.355479 | 27822.230855 | 573.597617 | 5633.411210 | 39.131613 | 10426.641638 | 11882.034020 | 2736.985774 | 6898.476895 | 17375.651891 | 86687.516992 | 0.959785 |
2707 rows × 12 columns
# Baseado no código de exemplo do professor João Gabriel de Moraes Souza
def get_value_at_risk(returns, confidence_level):
z_score = norm.ppf(confidence_level)
stdev = stocks_values['daily_return'].std()
var = -(returns.mean() + z_score * stdev)
return var
# Calculate VaR at the 90% confidence level
confidence_level = 0.90
returns = stocks_values['daily_return']
var_90 = get_value_at_risk(returns, confidence_level)
print(f'VaR no intervalo de confiância de 90%: {var_90:.2f} %')
VaR no intervalo de confiância de 90%: -2.17 %
np.random.seed(42)
returns = np.random.normal(best_return*100, best_vol*100, 10000)
# Calculate the VaR at the 90% confidence level
confidence_level = 0.90
var_90 = get_value_at_risk(returns, confidence_level)
# Plot the distribution of returns with the VaR level
fig, ax = plt.subplots(figsize=(12,8))
ax.hist(returns, bins=50, density=True, alpha=0.8)
ax.axvline(x=var_90, color='r', linestyle='--', label=f'VaR de {confidence_level*100:.0f}%')
ax.legend()
ax.set_xlabel('Retornos')
ax.set_ylabel('Densidade')
ax.set_title('Distribuição dos retornos com VaR até 90% de nível de confiância')
plt.show()
confidence_level = 0.90
returns = stocks_values['total_value']
var_90 = get_value_at_risk(returns, confidence_level)
print(f'VaR no intervalo de confiância de 90%: {var_90:.2f}')
VaR no intervalo de confiância de 90%: -72016.16
print(f"Media de ganho: R$ {stocks_values['total_value'].mean():.2f}")
print(f"Desvio padrão de ganho: R$ {stocks_values['total_value'].std():.2f}")
Media de ganho: R$ 72014.02 Desvio padrão de ganho: R$ 24735.20
np.random.seed(42)
returns = np.random.normal(stocks_values['total_value'].mean(), stocks_values['total_value'].std(), 10000)
confidence_level = 0.90
var_90 = get_value_at_risk(returns, confidence_level)
fig, ax = plt.subplots(figsize=(12,8))
ax.hist(returns, bins=50, density=True, alpha=0.8)
ax.axvline(x=var_90, color='r', linestyle='--', label=f'VaR de {confidence_level*100:.0f}%')
ax.legend()
ax.set_xlabel('Valor ganho com portfólio')
ax.set_ylabel('Densidade')
ax.set_title('Distribuição dos valores ganhos com VaR até 90% de nível de confiância')
plt.show()
Carteira aleatória¶
Agora, será gerada uma simulação com carteiras aleatórias compostas de 10 ações. A ideia é checar se o retorno médio das carteiras geradas suepra a estratégia com o modelo de Markowitz.
price_data = []
returns_data = []
for ticker in tqdm(list(available_tickers.keys())):
stock_data = available_tickers[ticker][
(available_tickers[ticker]['Date'] >= '2014-01-01') &
(available_tickers[ticker]['Date'] <= '2024-12-31')
].reset_index(drop=True)['Open']
stock_returns = np.log(stock_data / stock_data.shift(1))
price_data.append(stock_data)
returns_data.append(stock_returns)
random_portfolio = pd.concat(price_data, axis=1)
random_portfolio_returns = pd.concat(returns_data, axis=1)
random_portfolio.columns = list(available_tickers.keys())
random_portfolio_returns.columns = list(available_tickers.keys())
100%|███████████████████████████████| 127/127 [00:02<00:00, 60.44it/s]
random_portfolio_returns[["FRIO3", "CGAS3"]].std()
FRIO3 0.048195 CGAS3 0.025670 dtype: float64
random_portfolio
| FRIO3 | LPSB3 | CGAS3 | ELET3 | PTBL3 | TECN3 | CSNA3 | VALE3 | PLAS3 | USIM3 | ... | BBAS3 | CRIV3 | WEGE3 | SGPS3 | SBSP3 | CEBR3 | OIBR3 | CVCB3 | LIGT3 | ENEV3 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 92.689201 | 13.122008 | 15.086164 | 4.681541 | 3.176313 | 12.760195 | 8.622012 | 18.405229 | 23.0 | 9.813094 | ... | 6.237237 | 3.455436 | 3.801956 | 35.400002 | 20.036514 | 1.034179 | 2701.577148 | 11.255743 | 18.186223 | 43.206879 |
| 1 | 94.580818 | 13.140183 | 15.086167 | 4.388450 | 3.131122 | 12.314980 | 8.301578 | 17.841442 | 22.5 | 9.158887 | ... | 6.127543 | 3.455436 | 3.707029 | 35.200001 | 19.784143 | 1.067144 | 2634.410889 | 11.468390 | 17.783544 | 43.777267 |
| 2 | 92.689201 | 13.312840 | 14.833807 | 4.713226 | 3.079474 | 12.537586 | 8.533004 | 17.236645 | 22.5 | 9.797136 | ... | 6.127544 | 3.455436 | 3.663214 | 35.000000 | 19.692377 | 1.067144 | 3134.426514 | 11.695708 | 18.309492 | 46.058819 |
| 3 | 96.157166 | 12.867565 | 15.091963 | 4.847890 | 3.040738 | 12.219574 | 8.521135 | 17.431409 | 23.0 | 9.837027 | ... | 6.096931 | 3.455436 | 3.658348 | 36.000000 | 19.279408 | 1.067144 | 3097.112061 | 11.747036 | 18.243751 | 49.195953 |
| 4 | 93.319740 | 13.040222 | 15.091963 | 4.705306 | 3.047195 | 12.545536 | 8.455860 | 17.164894 | 22.0 | 9.908831 | ... | 5.994894 | 3.455436 | 3.679035 | 33.400002 | 19.332939 | 1.067144 | 3000.093994 | 11.666377 | 17.537009 | 49.195953 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2702 | 153.979996 | 1.670000 | 128.188766 | 35.590000 | 4.150000 | 5.060000 | 12.000000 | 61.990002 | 6.0 | 6.410000 | ... | 26.100000 | 6.980000 | 55.009998 | 1.640000 | 89.000000 | 18.950001 | 4.190000 | 2.020000 | 4.920000 | 12.140000 |
| 2703 | 153.000000 | 1.650000 | 128.188769 | 35.700001 | 4.100000 | 5.210000 | 11.580000 | 59.750000 | 6.0 | 6.120000 | ... | 26.100000 | 6.980000 | 54.660000 | 1.640000 | 89.550003 | 18.950001 | 4.200000 | 2.020000 | 4.870000 | 12.220000 |
| 2704 | 155.990005 | 1.620000 | 129.456037 | 35.840000 | 4.130000 | 5.430000 | 11.250000 | 58.580002 | 6.0 | 5.950000 | ... | 26.010000 | 6.980000 | 55.099998 | 1.640000 | 92.800003 | 18.559999 | 2.490000 | 2.070000 | 4.930000 | 12.250000 |
| 2705 | 160.000000 | 1.610000 | 128.589996 | 35.369999 | 4.120000 | 5.430000 | 10.900000 | 57.320000 | 6.0 | 5.970000 | ... | 25.990000 | 6.980000 | 55.160000 | 1.640000 | 95.900002 | 18.780001 | 1.010000 | 2.300000 | 4.910000 | 12.310000 |
| 2706 | 150.000000 | 1.570000 | 128.000000 | 35.410000 | 4.060000 | 5.470000 | 10.530000 | 57.160000 | 6.1 | 5.930000 | ... | 25.780001 | 6.980000 | 54.650002 | 1.640000 | 94.529999 | 18.770000 | 1.020000 | 2.170000 | 4.670000 | 11.900000 |
2707 rows × 127 columns
num_simulations = 50000
simulated_returns = np.zeros(num_simulations)
simulated_vol = np.zeros(num_simulations)
simulated_sharpe_ratio = np.zeros(num_simulations)
final_portfolio_values = np.zeros(num_simulations)
initial_investment = 35000
10 ações¶
for i in tqdm(range(num_simulations)):
random_tickers = random.choices(list(available_tickers.keys()), k=10)
simulated_returns[i] = random_portfolio_returns[random_tickers].mean().mean()
weights = np.ones(10) / 10
simulated_vol[i] = np.sqrt(np.dot(weights.T, np.dot(random_portfolio_returns[random_tickers].cov(), weights)))
simulated_sharpe_ratio[i] = simulated_returns[i]/simulated_vol[i]
cumulative_log_return = random_portfolio_returns[random_tickers].mean(axis=1).sum()
growth_factor = np.exp(cumulative_log_return)
final_portfolio_values[i] = initial_investment * growth_factor
100%|███████████████████████████| 50000/50000 [09:53<00:00, 84.24it/s]
print(f'Média do Sharpe Ratio: {simulated_sharpe_ratio.mean()}')
Média do Sharpe Ratio: -0.002915816655464186
print(f'Média dos retornos: {simulated_returns.mean()}')
Média dos retornos: -6.774172907292032e-05
print(f'Média dos ganhos: R$ {final_portfolio_values.mean()}')
Média dos ganhos: R$ 36852.19181708624
print(f'Risco médio para ganhos maiores que carteira com modelo de Markowitz (R$ {total_sum:.2f}): '
f'{simulated_vol[np.where(final_portfolio_values > total_sum)].mean()}')
Risco médio para ganhos maiores que carteira com modelo de Markowitz (R$ 86687.52): 0.014890269155231407
print(f'Sharpe Ratio médio para ganhos maiores que carteira com modelo de Markowitz (R$ {total_sum}): '
f'{simulated_sharpe_ratio[np.where(final_portfolio_values > total_sum)].mean()}')
Sharpe Ratio médio para ganhos maiores que carteira com modelo de Markowitz (R$ 86687.51699174444): 0.026975447050837105
print('Número de carteiras melhores que carteira com modelo de Markowitz:',
len(final_portfolio_values[final_portfolio_values > total_sum]))
Número de carteiras melhores que carteira com modelo de Markowitz: 1814
print('Porcentagem de carteiras com ganho: '
f'{100*len(final_portfolio_values[final_portfolio_values > 35000])/len(final_portfolio_values)} %')
Porcentagem de carteiras com ganho: 45.962 %
plt.figure(figsize=(10, 6))
plt.hist(final_portfolio_values, bins=50, color='blue', edgecolor='black', alpha=0.7)
plt.title("Distribuição dos ganhos em reais (carteiras com 10 ações)")
plt.xlabel("Valor")
plt.ylabel("Frequência")
plt.ticklabel_format(style='plain', axis='x')
plt.show()
20 ações¶
for i in tqdm(range(num_simulations)):
random_tickers = random.choices(list(available_tickers.keys()), k=20)
simulated_returns[i] = random_portfolio_returns[random_tickers].mean().mean()
weights = np.ones(20) / 20
simulated_vol[i] = np.sqrt(np.dot(weights.T, np.dot(random_portfolio_returns[random_tickers].cov(), weights)))
simulated_sharpe_ratio[i] = simulated_returns[i]/simulated_vol[i]
cumulative_log_return = random_portfolio_returns[random_tickers].mean(axis=1).sum()
growth_factor = np.exp(cumulative_log_return)
final_portfolio_values[i] = initial_investment * growth_factor
100%|███████████████████████████| 50000/50000 [13:58<00:00, 59.60it/s]
print(f'Média do Sharpe Ratio: {simulated_sharpe_ratio.mean()}')
Média do Sharpe Ratio: -0.003969698457325113
print(f'Média dos retornos: {simulated_returns.mean()}')
Média dos retornos: -6.789250316767901e-05
print(f'Média dos ganhos: R$ {final_portfolio_values.mean()}')
Média dos ganhos: R$ 33119.45721772785
print(f'Risco médio para ganhos maiores que carteira com modelo de Markowitz (R$ {total_sum:.2f}): '
f'{simulated_vol[np.where(final_portfolio_values > total_sum)].mean()}')
Risco médio para ganhos maiores que carteira com modelo de Markowitz (R$ 86687.52): 0.01335952284738918
print(f'Sharpe Ratio médio para ganhos maiores que carteira com modelo de Markowitz (R$ {total_sum}): '
f'{simulated_sharpe_ratio[np.where(final_portfolio_values > total_sum)].mean()}')
Sharpe Ratio médio para ganhos maiores que carteira com modelo de Markowitz (R$ 86687.51699174444): 0.027628862844659735
print('Número de carteiras melhores que carteira com modelo de Markowitz:',
len(final_portfolio_values[final_portfolio_values > total_sum]))
Número de carteiras melhores que carteira com modelo de Markowitz: 224
print('Porcentagem de carteiras com ganho: '
f'{100*len(final_portfolio_values[final_portfolio_values > 35000])/len(final_portfolio_values)} %')
Porcentagem de carteiras com ganho: 40.632 %
plt.figure(figsize=(10, 6))
plt.hist(final_portfolio_values, bins=50, color='blue', edgecolor='black', alpha=0.7)
plt.title("Distribuição dos ganhos em reais (carteiras com 20 ações)")
plt.xlabel("Valor")
plt.ylabel("Frequência")
plt.ticklabel_format(style='plain', axis='x')
plt.show()
Conclusões¶
- Comparando a melhor carteira com o modelo de Markowitz e as melhores carteiras aleatórias (seja para 10 ou 20 ativos), vemos que o risco média das carteiras aleatórias para situações que elas são mais vantajosa é maior que o risco apresentado pela carteira ótima de Markowitz;
- Os filtros usados para selecionar os 10 ativos no caso do modelo de Markowitz com certeza tem influência nos resultados (ações do Ibovespa e com maior crescimento entre janeiro e fevereiro de 2014), mas também conseguem indicar o seguinte: mesmo sem nenhum tipo de filtro, considerando só ações que tem o código existente até 2024, seria possível "ganhar dinheiro" (ignorando a inflação, obviamente) em cerca de 40% das vezes;
- O que, em uma análise simples, leva às seguintes conclusões:
- Diversificação é um fator importantíssimo (já provado inúmeras vezes) para diluição de risco, independente da forma de escolher e balancear os ativos;
- Empresas boas (com lucro recorrente), a longo prazo, irão gerar resultados mais consistentes e mais valor, o que irá refletir em seu preço;
- Por último: estatisticamente falando, a longo prazo inclusive, a chance maior é de não bater o mercado (como o gráfico das carteiras aleatórias mostra). Nesse sentido, para pessoas comuns (e sem informações internas), talvez o melhor seja investir em índices passivos, que refletem uma cesta de ações de determinado mercado e tem taxas de administração mais baixas que fundos tradicionais, o que, em tese, acompanharia a média do mercado, que a longo prazo (até hoje, pelo menos) tende a crescer. A ideia já foi defendida até por Buffet, que supostamente incluiu em seu testamento um pedido ao administrador da herança: "coloque 10% do dinheiro em títulos públicos de curto prazo e 90% em um fundo de índice S&P 500 de custo muito baixo".